﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.IO;

namespace HexMerge
{
    public partial class frmMain : Form
    {
        OffsetDialog offsetDialog = new OffsetDialog();
        OpenFileDialog selectFilesDialog = new OpenFileDialog();
        SaveFileDialog saveFileDialog = new SaveFileDialog();

        public frmMain(string[] args)
        {
            InitializeComponent();

            selectFilesDialog.Title = "Select Files";
            selectFilesDialog.Filter = "Hex Files(*.hex)|*.hex|Binary Files(*.bin)|*.bin|All Files(*.*)|*.*";
            selectFilesDialog.Multiselect = true;

            saveFileDialog.Title = "Save File";
            saveFileDialog.Filter = "Hex Files(*.hex)|*.hex|Binary Files(*.bin)|*.bin|All Files(*.*)|*.*";
            saveFileDialog.FileName = "out_merge.hex";

            if (HexMergeProgram.HexMergeMain(this, args))
            {
                this.Close();
            }
        }

        private void listViewFile_Resize(object sender, EventArgs e)
        {
            listViewFile.AutoResizeColumn(3, ColumnHeaderAutoResizeStyle.HeaderSize);
        }

        public void showMessage(string msg)
        {
            //textBoxMessage.AppendText(DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + "\r\n");
            textBoxMessage.AppendText(msg + "\r\n");
        }

        private void listViewFileIndexUpdate()
        {
            for (int i = 0; i < listViewFile.Items.Count; i++)
            {
                listViewFile.Items[i].Text = (i + 1).ToString();
            }
        }

        private string pathFileName(string path)
        {
            int p = path.LastIndexOf("\\");
            if (p < 0)
            {
                return "";
            }

            return path.Substring(p + 1);
        }

        private string pathExtension(string path)
        {
            int p = path.LastIndexOf(".");
            if (p < 0)
            {
                return "";
            }

            return path.Substring(p + 1);
        }

        private void listViewFileAppend(string[] FileNames)
        {
            foreach (string path in FileNames)
            {
                if (listViewFile.Items.Count == 0 || listViewFile.FindItemWithText(path, true, 0) == null)
                {
                    string fileExtName = pathExtension(path);
                    if (fileExtName.ToLower() == "hex")
                    {
                        ListViewItem item = listViewFile.Items.Add((listViewFile.Items.Count + 1).ToString());
                        item.SubItems.Add(fileExtName);
                        item.SubItems.Add("");
                        item.SubItems.Add(path);

                        showMessage("Add File  " + path);
                    }
                    else
                    {
                        offsetDialog.SetFileName(pathFileName(path));
                        if (offsetDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                        {
                            string offset = offsetDialog.GetOffset();
                            ListViewItem item = listViewFile.Items.Add((listViewFile.Items.Count + 1).ToString());
                            item.SubItems.Add(fileExtName);
                            item.SubItems.Add(offset);
                            item.SubItems.Add(path);

                            showMessage("Add File(@" + offset + ") " + path);
                        }
                    }
                }

                buttonMerge.Enabled = listViewFile.Items.Count > 0;
            }
        }

        private void buttonAppend_Click(object sender, EventArgs e)
        {
            if (selectFilesDialog.ShowDialog() == DialogResult.OK)
            {
                listViewFileAppend(selectFilesDialog.FileNames);
            }
        }

        private void buttonDelete_Click(object sender, EventArgs e)
        {
            int itemFirst = -1;
            foreach (ListViewItem item in listViewFile.SelectedItems)
            {
                if (itemFirst < 0)
                {
                    itemFirst = item.Index;
                }
                item.Remove();
                showMessage("Remove File " + item.SubItems[3].Text);

                buttonMerge.Enabled = listViewFile.Items.Count > 0;
            }
            if (listViewFile.Items.Count > 0)
            {
                if (itemFirst >= listViewFile.Items.Count)
                {
                    itemFirst = listViewFile.Items.Count - 1;
                }
                listViewFile.Items[itemFirst].Selected = true;
            }
            listViewFileIndexUpdate();
        }

        private void listViewFile_SelectedIndexChanged(object sender, EventArgs e)
        {
            buttonDelete.Enabled = listViewFile.SelectedItems.Count > 0;
        }

        private void buttonMerge_Click(object sender, EventArgs e)
        {
            List<string> args = new List<string>();

            if (saveFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                foreach (ListViewItem item in listViewFile.Items)
                {
                    if (item.SubItems[1].Text == "hex")
                    {
                        args.Add("-i");
                        args.Add(item.SubItems[3].Text);
                    }
                    else
                    {
                        args.Add("-ib");
                        args.Add(item.SubItems[2].Text);
                        args.Add(item.SubItems[3].Text);
                    }
                }

                if (pathExtension(saveFileDialog.FileName).ToLower() == "hex")
                {
                    args.Add("-o");
                }
                else
                {
                    args.Add("-ob");
                }
                args.Add(saveFileDialog.FileName);

                HexMergeProgram.HexMergeMain(this, args.ToArray());
            }
        }

        private void listViewFile_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
            else
            {
                e.Effect = DragDropEffects.None;
            }
        }

        private void listViewFile_DragDrop(object sender, DragEventArgs e)
        {
            String[] FileNames = e.Data.GetData(DataFormats.FileDrop, false) as String[];

            listViewFileAppend(FileNames);
        }
    }

    class HexMergeProgram
    {
        static frmMain _frmMain;

        static public bool HexMergeMain(frmMain frmMain, string[] args)
        {
            _frmMain = frmMain;

            if (args.Length == 0)
            {
                _frmMain.showMessage("e.g: HexMerge -i loader.hex app.hex -o out_merge.hex\r\n");
                _frmMain.showMessage("HexMerge [-i file1.hex[ file2.hex]...]");
                _frmMain.showMessage(" [-ib offset1 file1.bin[ offset2 file2.bin]...]");
                _frmMain.showMessage(" [-o out_merge.hex]");
                _frmMain.showMessage(" [-ob [out_offset] out_merge.bin]");
                _frmMain.showMessage("(offset is Decimal or Hexadecimal)");
                return false;
            }

            try
            {
                int cmd_state = 0;
                HexMeta[] hex_tmp;
                UInt32 file_offset_tmp = 0;

                string file_name = "";
                bool bin_file_offset_auto = false;
                UInt32 bin_file_offset = 0;
                string bin_file_name = "";
                List<HexMeta> hex_list = new List<HexMeta>();

                for (int i = 0; i < args.Length; i++)
                {
                    if (args[i] == "-i")
                    {
                        cmd_state = 0;
                        continue;
                    }
                    if (args[i] == "-o")
                    {
                        cmd_state = 1;
                        continue;
                    }
                    if (args[i] == "-ib")
                    {
                        cmd_state = 2;
                        continue;
                    }
                    if (args[i] == "-ob")
                    {
                        cmd_state = 3;
                        continue;
                    }

                    switch (cmd_state)
                    {
                    case 0:
                        _frmMain.showMessage("Merge File " + args[i] + "...");
                        hex_tmp = HexFileRead(args[i]);
                        if (hex_tmp.Length == 0)
                        {
                            _frmMain.showMessage("HexFileRead error:" + args[i]);
                            return false;
                        }
                        hex_list.AddRange(hex_tmp);
                        break;
                    case 1:
                        file_name = args[i];
                        break;
                    case 2:
                        if (!UInt32.TryParse(args[i], out file_offset_tmp))
                        {
                            file_offset_tmp = UInt32.Parse(args[i].ToLower().Replace("0x", " "), System.Globalization.NumberStyles.HexNumber);
                        }
                        i++;
                        _frmMain.showMessage("Merge File " + args[i] + "...");
                        hex_tmp = BinFileRead(args[i], file_offset_tmp);
                        if (hex_tmp.Length == 0)
                        {
                            _frmMain.showMessage("BinFileRead error:" + args[i]);
                            return false;
                        }
                        hex_list.AddRange(hex_tmp);
                        break;
                    case 3:
                        if (!UInt32.TryParse(args[i], out file_offset_tmp))
                        {
                            if (!UInt32.TryParse(args[i].ToLower().Replace("0x", " "), System.Globalization.NumberStyles.HexNumber,
                                System.Globalization.CultureInfo.InvariantCulture, out file_offset_tmp))
                            {
                                bin_file_offset_auto = true;
                            }
                        }
                        if (!bin_file_offset_auto)
                        {
                            bin_file_offset = file_offset_tmp;
                            i++;
                        }
                        bin_file_name = args[i];
                        break;
                    }
                }

                if ((file_name.Length == 0) && (bin_file_name.Length == 0))
                {
                    file_name = "out_merge.hex";
                }

                HexMeta[] hex_merge = HexMetaMerge(hex_list.ToArray());

                if (file_name.Length > 0)
                {
                    HexFileWrite(file_name, hex_merge);
                    _frmMain.showMessage("Output File " + file_name);
                }
                if (bin_file_name.Length > 0)
                {
                    if (bin_file_offset_auto)
                    {
                        bin_file_offset = hex_merge[HexMetaOffsetMin(hex_merge)].offset;
                    }
                    BinFileWrite(bin_file_name, hex_merge, bin_file_offset);
                    _frmMain.showMessage("Output File " + bin_file_name);
                }
            }
            catch (Exception ex)
            {
                _frmMain.showMessage("Error:" + ex.Message);
                return false;
            }

            return true;
        }

        struct HexMeta
        {
            public UInt32 offset;
            public List<Byte> data;
        }

        static Byte[] LineDecode(string line_str)
        {
            if ((line_str.Length < 1) || (line_str.Substring(0, 1) != ":"))
            {
                return null;
            }

            int bytes_length = (line_str.Length - 1) / 2;
            Byte[] bytes = new Byte[bytes_length];
            Byte check_sum = 0;

            for (int i = 0; i < bytes_length; i++)
            {
                bytes[i] = Byte.Parse(line_str.Substring(1 + i * 2, 2), System.Globalization.NumberStyles.HexNumber);
                check_sum += bytes[i];
            }

            if ((check_sum != 0) || (bytes[0] != bytes_length - 5))
            {
                return null;
            }

            return bytes;
        }

        static string HexFormat(Byte num)
        {
            return (num + 256).ToString("x").Substring(1);
        }

        static string LineEncode(UInt16 offset, Byte type, Byte[] data)
        {
            return LineEncode(offset, type, data, 0, data.Length);
        }

        static string LineEncode(UInt16 offset, Byte type, Byte[] data, int data_offset, int data_length)
        {
            StringBuilder sb = new StringBuilder();
            Byte check_sum = 0;

            sb.Append(":");
            sb.Append(HexFormat((Byte)(data_length)));
            check_sum += (Byte)(data_length);
            sb.Append(HexFormat((Byte)(offset >> 8)));
            check_sum += (Byte)(offset >> 8);
            sb.Append(HexFormat((Byte)(offset & 0xFF)));
            check_sum += (Byte)(offset & 0xFF);
            sb.Append(HexFormat(type));
            check_sum += type;
            for (int i = 0; i < data_length; i++)
            {
                sb.Append(HexFormat(data[data_offset + i]));
                check_sum += data[data_offset + i];
            }
            sb.Append(HexFormat((Byte)(-check_sum)));

            return sb.ToString().ToUpper();
        }

        static HexMeta[] HexMetaMerge(HexMeta[] hex_meta)
        {
            int merge_count = 1;

            while (merge_count > 0)
            {
                hex_meta = HexMetaMergeEx(hex_meta, ref merge_count);
            }

            return hex_meta;
        }
        static HexMeta[] HexMetaMergeEx(HexMeta[] hex_meta, ref int merge_count)
        {
            List<HexMeta> hex_list = new List<HexMeta>();

            merge_count = 0;
            for (int i = 0; i < hex_meta.Length; i++)
            {
                bool new_flag = true;
                for (int j = 0; j < hex_list.Count; j++)
                {
                    if (hex_meta[i].offset < hex_list[j].offset)
                    {
                        if (hex_meta[i].offset + hex_meta[i].data.Count == hex_list[j].offset)
                        {
                            merge_count++;
                            HexMeta meta_tmp = new HexMeta();
                            meta_tmp.offset = (UInt32)(hex_list[j].offset - hex_meta[i].data.Count);
                            meta_tmp.data = hex_list[j].data;
                            meta_tmp.data.InsertRange(0, hex_meta[i].data);
                            hex_list[j] = meta_tmp;
                            new_flag = false;
                            break;
                        }
                        else if (hex_meta[i].offset + hex_meta[i].data.Count > hex_list[j].offset)
                        {
                            _frmMain.showMessage("Overlapping!!(" + hex_meta[i].offset.ToString() + " + " + hex_meta[i].data.Count.ToString() + " > " + hex_list[j].offset.ToString() + ")");
                            merge_count++;
                            HexMeta meta_tmp = new HexMeta();
                            meta_tmp.offset = (UInt32)(hex_list[j].offset - hex_meta[i].offset);
                            meta_tmp.data = hex_list[j].data;
                            meta_tmp.data.InsertRange(0, hex_meta[i].data.Take((int)(hex_list[j].offset - hex_meta[i].offset)));
                            hex_list[j] = meta_tmp;
                            new_flag = false;
                            break;
                        }
                        else if ((j == 0) || (hex_meta[i].offset > hex_list[j - 1].offset))
                        {
                            merge_count++;
                            hex_list.Insert(j, hex_meta[i]);
                            new_flag = false;
                            break;
                        }
                    }
                    else
                    {
                        if (hex_meta[i].offset == hex_list[j].offset + hex_list[j].data.Count)
                        {
                            merge_count++;
                            hex_list[j].data.AddRange(hex_meta[i].data);
                            new_flag = false;
                            break;
                        }
                        else if (hex_meta[i].offset < hex_list[j].offset + hex_list[j].data.Count)
                        {
                            _frmMain.showMessage("Overlapping!!(" + hex_meta[i].offset.ToString() + " < " + hex_list[j].offset.ToString() + " + " + hex_list[j].data.Count.ToString() + ")");
                            if (hex_meta[i].offset + hex_meta[i].data.Count > hex_list[j].offset + hex_list[j].data.Count)
                            {
                                merge_count++;
                                hex_list[j].data.AddRange(hex_meta[i].data.Skip((int)(hex_list[j].offset + hex_list[j].data.Count - hex_meta[i].offset)));
                            }
                            new_flag = false;
                            break;
                        }
                        else if (j > 0)
                        {
                            if (hex_meta[i].offset < hex_list[j - 1].offset)
                            {
                                merge_count++;
                                hex_list.Insert(j, hex_meta[i]);
                                new_flag = false;
                                break;
                            }
                        }
                    }

                }
                if (new_flag)
                {
                    hex_list.Add(hex_meta[i]);
                }
            }

            return hex_list.ToArray();
        }

        static int HexMetaOffsetMin(HexMeta[] hex_meta)
        {
            int index = 0;
            UInt32 offset = hex_meta[index].offset;

            for (int i = 1; i < hex_meta.Length; i++)
            {
                if (offset > hex_meta[i].offset)
                {
                    index = i;
                    offset = hex_meta[index].offset;
                }
            }

            return index;
        }

        static int HexMetaOffsetMax(HexMeta[] hex_meta)
        {
            int index = 0;
            UInt32 offset = hex_meta[index].offset;

            for (int i = 1; i < hex_meta.Length; i++)
            {
                if (offset < hex_meta[i].offset)
                {
                    index = i;
                    offset = hex_meta[index].offset;
                }
            }

            return index;
        }

        static HexMeta[] HexFileRead(string file)
        {
            List<HexMeta> hex_list = new List<HexMeta>();

            try
            {
                FileStream fs = new FileStream(file, FileMode.Open);
                StreamReader sr = new StreamReader(fs);
                UInt32 line_address = 0;
                bool loop_flag = true;
                while (loop_flag)
                {
                    Byte[] bytes = LineDecode(sr.ReadLine().Trim());
                    if (bytes == null)
                    {
                        _frmMain.showMessage("Line decode error!!");
                        break;
                    }

                    switch (bytes[3])
                    {
                    case 0x00: // Data Rrecord
                        HexMeta meta_tmp = new HexMeta();
                        meta_tmp.offset = line_address + (UInt32)((UInt16)bytes[1] * 256 + bytes[2]);
                        meta_tmp.data = bytes.Skip(4).Take(bytes[0]).ToList();
                        hex_list.Add(meta_tmp);
                        break;
                    case 0x01: // End of File Record
                        //_frmMain.showMessage("Ignore data type " + bytes[3].ToString());
                        loop_flag = false;
                        break;
                    case 0x02: // Extended Segment Address Record
                        _frmMain.showMessage("Ignore data type " + bytes[3].ToString());
                        break;
                    case 0x03: // Start Segment Address Record
                        _frmMain.showMessage("Ignore data type " + bytes[3].ToString());
                        break;
                    case 0x04: // Extended Linear Address Record
                        line_address = (UInt32)((UInt16)bytes[4] * 256 + bytes[5]) * 65536;
                        break;
                    case 0x05: // Start Linear Address Record
                        _frmMain.showMessage("Ignore data type " + bytes[3].ToString());
                        break;
                    default:
                        _frmMain.showMessage("Error data type " + bytes[3].ToString());
                        loop_flag = false;
                        break;
                    }
                }
                sr.Close();
                fs.Close();
            }
            catch (Exception ex)
            {
                _frmMain.showMessage(ex.Message);
                hex_list.Clear();
            }

            return hex_list.ToArray();
        }

        static void HexFileWrite(string file, HexMeta[] hex_meta)
        {
            FileStream fs = new FileStream(file, FileMode.Create);
            StreamWriter sw = new StreamWriter(fs);
            UInt16 line_address = 0;
            const int LINE_BYTES = 32;
            for (int i = 0; i < hex_meta.Length; i++)
            {
                UInt32 offset = hex_meta[i].offset;
                byte[] data = hex_meta[i].data.ToArray();
                for (int j = 0; j < data.Length; j += LINE_BYTES)
                {
                    UInt16 line_address_tmp = (UInt16)(offset >> 16);
                    if (line_address != line_address_tmp)
                    {
                        line_address = line_address_tmp;
                        Byte[] bytes = new Byte[2];
                        bytes[0] = (Byte)(line_address >> 8);
                        bytes[1] = (Byte)(line_address & 0xFF);
                        sw.WriteLine(LineEncode(0, 0x04, bytes));
                    }

                    int n = data.Length - j;
                    if (n > LINE_BYTES)
                    {
                        n = LINE_BYTES;
                    }
                    sw.WriteLine(LineEncode((UInt16)(offset & 0xFFFF), 0x00, data, j, n));
                    offset += (UInt32)n;
                }
            }
            sw.WriteLine(LineEncode(0, 0x01, new Byte[0]));
            sw.Close();
            fs.Close();
        }

        static HexMeta[] BinFileRead(string file, UInt32 offset)
        {
            List<HexMeta> hex_list = new List<HexMeta>();

            try
            {
                HexMeta meta_tmp = new HexMeta();
                meta_tmp.offset = offset;
                meta_tmp.data = new List<byte>();
                meta_tmp.data.AddRange(File.ReadAllBytes(file));

                hex_list.Add(meta_tmp);
            }
            catch (Exception ex)
            {
                _frmMain.showMessage(ex.Message);
                hex_list.Clear();
            }

            return hex_list.ToArray();
        }

        static void BinFileWrite(string file, HexMeta[] hex_meta, UInt32 offset)
        {
            FileStream fs = new FileStream(file, FileMode.Create);

            {
                int i = HexMetaOffsetMax(hex_meta);
                if (hex_meta[i].offset < offset)
                {
                    throw (new Exception("Output file offset error!!"));
                }
                for (int j = 0; j < hex_meta[i].offset - offset + hex_meta[i].data.Count; j++)
                {
                    fs.WriteByte(0xFF);
                }
            }

            for (int i = 0; i < hex_meta.Length; i++)
            {
                if (hex_meta[i].offset < offset)
                {
                    throw (new Exception("Output file offset error!!"));
                }
                fs.Seek(hex_meta[i].offset - offset, SeekOrigin.Begin);
                fs.Write(hex_meta[i].data.ToArray(), 0, hex_meta[i].data.Count);
            }
            fs.Close();
        }
    }
}
